package com.fansin.hyperledger.fabric.java.config;

import com.fansin.hyperledger.fabric.java.FabricManager;
import com.fansin.hyperledger.fabric.java.FabricUtil;
import com.fansin.hyperledger.fabric.java.store.SampleStore;
import com.fansin.hyperledger.fabric.java.store.SampleUser;
import lombok.extern.slf4j.Slf4j;
import org.hyperledger.fabric.sdk.*;
import org.hyperledger.fabric.sdk.exception.CryptoException;
import org.hyperledger.fabric.sdk.exception.ProposalException;
import org.hyperledger.fabric.sdk.exception.TransactionException;
import org.hyperledger.fabric.sdk.helper.Config;
import org.hyperledger.fabric.sdk.security.CryptoSuite;
import org.hyperledger.fabric_ca.sdk.EnrollmentRequest;
import org.hyperledger.fabric_ca.sdk.HFCAClient;
import org.hyperledger.fabric_ca.sdk.RegistrationRequest;
import org.hyperledger.fabric_ca.sdk.exception.InvalidArgumentException;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hyperledger.fabric.sdk.Channel.PeerOptions.createPeerOptions;

/**
 * <p>Title: FabricCofig</p>
 * <p>Description: </p>
 *
 * 官方详细的
 *
 *@see Config
 *
 * @author zhaofeng
 * @version 1.0
 * @date 18-8-31
 */
@Configuration
@Slf4j
@Order
public class FabricAutoConfiguration implements ApplicationRunner {


    private static final  String                CONFIG_OVERRIDES = "FABRICSDKOVERRIDES";

    @Resource
    private FabricConfiguration configuration;

    /**
     * Callback used to run the bean.
     *
     * @param args incoming application arguments
     * @throws Exception on error
     */
    @Override
    public void run(ApplicationArguments args) {

        try {
            log.info("加载配置文件 开始...");
            initEnvironment();
            log.info("加载配置文件 完成...");
            log.info("向ca服务器背书 开始...");
            enroll();
            log.info("向ca服务器背书 完成...");
            log.info("创建channel 开始...");
            createChannel();
            log.info("创建channel 完成...");
        }catch (Exception e){
            log.error("初始化服务失败!",e);
        }
    }

    public void initEnvironment() throws MalformedURLException, InvalidArgumentException, IllegalAccessException,
                                         InvocationTargetException, org.hyperledger.fabric.sdk.exception.InvalidArgumentException, InstantiationException, NoSuchMethodException,
                                         CryptoException, ClassNotFoundException {
        try {
            final Field field = Config.class.getDeclaredField("config");
            field.setAccessible(true);
            field.set(Config.class, null);
            Config.getConfig();
        } catch (Exception e) {
            throw new RuntimeException("Cannot reset config", e);
        }

        String fabricSdkConfig = System.getenv(CONFIG_OVERRIDES);
        log.info("FABRICSDKOVERRIDES={} ",fabricSdkConfig);
        if (fabricSdkConfig != null && fabricSdkConfig.length() > 0) {
            String[] configs = fabricSdkConfig.split(",");
            String[] configKeyValue;
            for (String config : configs) {
                configKeyValue = config.split("=");
                if (configKeyValue != null && configKeyValue.length == 2) {
                    System.setProperty(configKeyValue[0], configKeyValue[1]);
                }
            }
        }

        for (FabricConfiguration.Organization organization : configuration.getOrganizations().values()) {
            //Try one of each name and no name.
            String caName = organization.getCaName();
            if (caName != null && !caName.isEmpty()) {
                organization.setCaClient(
                        HFCAClient.createNewInstance(caName, organization.getCaLocation(), organization.getCaProperties()));
            } else {
                organization.setCaClient(HFCAClient.createNewInstance(organization.getCaLocation(), organization.getCaProperties()));
            }

            organization.getCaClient().setCryptoSuite(CryptoSuite.Factory.getCryptoSuite());
        }
    }


    public void enroll() throws Exception {
        SampleStore sampleStore = FabricManager.getSampleStore();
        for (FabricConfiguration.Organization organization : configuration.getOrganizations().values()) {

            String orgName = organization.getName();
            String mspid = organization.getMspid();

            if (configuration.getRunningTLS()) {
                //This shows how to get a client TLS certificate from Fabric CA
                // we will use one client TLS certificate for orderer peers etc.
                final EnrollmentRequest enrollmentRequestTLS = new EnrollmentRequest();
                //TODO 这里可配置
                enrollmentRequestTLS.addHost(configuration.getEnrollEnvironment().getHost());
                enrollmentRequestTLS.setProfile(configuration.getEnrollEnvironment().getProfile());
                //去ca拿到key和cert信息
                final Enrollment enroll = organization.getCaClient().enroll(configuration.getEnrollEnvironment().getUser(), configuration.getEnrollEnvironment().getSecret(), enrollmentRequestTLS);
                //未设置,则自动生成ECDSA公私钥
                final String tlsCertPEM = enroll.getCert();
                final String tlsKeyPEM = FabricUtil.getPEMStringFromPrivateKey(enroll.getKey());

                final Properties tlsProperties = new Properties();

                tlsProperties.put("clientKeyBytes", tlsKeyPEM.getBytes(UTF_8));
                tlsProperties.put("clientCertBytes", tlsCertPEM.getBytes(UTF_8));
                configuration.getClientTLSProperties().put(orgName, tlsProperties);
                //Save in samplestore for follow on tests.
                sampleStore.storeClientPEMTLCertificate(organization, tlsCertPEM);
                sampleStore.storeClientPEMTLSKey(organization, tlsKeyPEM);
            }


            //TODO 可配置用户 TEST_ADMIN_NAME
            SampleUser admin = sampleStore.getMember(configuration.getEnrollEnvironment().getUser(), orgName);
            if (!admin.isEnrolled()) {  //Preregistered admin only needs to be enrolled with Fabric caClient.
                //首次为空
                admin.setEnrollment(organization.getCaClient().enroll(admin.getName(), configuration.getEnrollEnvironment().getSecret()));
                admin.setMspId(mspid);
            }
//            System.out.printf("配置admin用户 %s \n",new GsonBuilder().create().toJson(admin));
            organization.setAdmin(admin); // The admin of this org --

            //TODO 配置TESTUSER_1_NAME 同 TEST_ADMIN_NAME
            for (Map.Entry<String, SampleUser> userEntry: organization.getUserMap().entrySet()) {
                String userName = userEntry.getKey();
                SampleUser user = userEntry.getValue();
                if (!user.isRegistered()) {  // users need to be registered AND enrolled
                    RegistrationRequest rr = new RegistrationRequest(userName, user.getAffiliation());
                    user.setEnrollmentSecret(organization.getCaClient().register(rr, admin));
                }
                if (!user.isEnrolled()) {
                    user.setEnrollment(organization.getCaClient().enroll(user.getName(), user.getEnrollmentSecret()));
                    user.setMspId(mspid);
                }
//            System.out.printf("配置user用户 %s \n",new GsonBuilder().create().toJson(user));
                organization.addUser(user); //Remember user belongs to this Org
            }

            //TODO 配置 peerOrgAdmin
            File privateKeyFile = Util.findFileSk(Paths.get(organization.getPrivateKeyDir()).toFile());
            File certificateFile = Paths.get(organization.getCertificateFile()).toFile();
            SampleUser peerOrgAdmin = sampleStore.getMember(orgName + "Admin",
                                                            orgName,
                                                            mspid,
                                                            privateKeyFile,
                                                            certificateFile
            );

//            System.out.printf("配置peerOrgAdmin用户 %s \n",new GsonBuilder().create().toJson(peerOrgAdmin));
            organization.setPeerAdmin(peerOrgAdmin); //A special user that can create channels, join peers and install chaincode

        }
    }



    public void createChannel()
            throws org.hyperledger.fabric.sdk.exception.InvalidArgumentException, IOException, TransactionException,
                   ProposalException, ClassNotFoundException {
        HFClient client = FabricManager.newClient();

        Collection<Orderer> orderers = new LinkedList<>();
        for (FabricConfiguration.Organization organization : configuration.getOrganizations().values()) {
            client.setUserContext(organization.getPeerAdmin());
            for (Map.Entry<String, String> entry : organization.getOrdererLocations().entrySet()) {
                String orderName = entry.getKey();
                String orderLocation = entry.getValue();
                //加载order
                Properties ordererProperties = configuration.getOrdererProperties(entry.getKey());

                //TODO 可配置
                //example of setting keepAlive to avoid timeouts on inactive http2 connections.
                // Under 5 minutes would require changes to server side to accept faster ping rates.
                ordererProperties.put("grpc.NettyChannelBuilderOption.keepAliveTime", new Object[] {5L, TimeUnit.MINUTES});
                ordererProperties.put("grpc.NettyChannelBuilderOption.keepAliveTimeout", new Object[] {8L, TimeUnit.SECONDS});
                ordererProperties.put("grpc.NettyChannelBuilderOption.keepAliveWithoutCalls", new Object[] {true});

                orderers.add(client.newOrderer(orderName, orderLocation,
                                               ordererProperties));
            }

            //Just pick the first orderer in the list to create the channel.

            Orderer anOrderer = orderers.iterator().next();
            orderers.remove(anOrderer);

            //加载 *.tx
            ChannelConfiguration channelConfiguration = new ChannelConfiguration(Paths.get(organization.getChannelConfig().getChannelConfigFile()).toFile());
//            System.out.printf("加载 ChannelConfiguration:%s \n",new GsonBuilder().create().toJson(channelConfiguration));
            //Create channel that has only one signer that is this orgs peer admin. If channel creation policy needed more signature they would need to be added too.
            //广播order 获取创世区块
//            if(client.getChannel(organization.getChannelConfig().getName())!=null){
//                log.info("客户端已存在channel");
//            }
            Channel newChannel = client.newChannel(organization.getChannelConfig().getName(), anOrderer, channelConfiguration, client.getChannelConfigurationSignature(channelConfiguration, organization.getPeerAdmin()));

            //test with both cases when doing peer eventing.
            boolean everyother = true;
            for (Map.Entry<String, String> peerEntry : organization.getPeerLocations().entrySet()) {
                String peerName = peerEntry.getKey();
                String peerLocation = peerEntry.getValue();

                Properties peerProperties = configuration.getPeerProperties(peerName,organization.getName()); //test properties for peer.. if any.
                if (peerProperties == null) {
                    peerProperties = new Properties();
                }

                //Example of setting specific options on grpc's NettyChannelBuilder
                peerProperties.put("grpc.NettyChannelBuilderOption.maxInboundMessageSize", 9000000);

                //是否启用事件通知
                Peer peer = client.newPeer(peerName, peerLocation, peerProperties);
                if (everyother) {
                    //Default is all roles.
                    newChannel.joinPeer(peer, createPeerOptions().setPeerRoles(
                            EnumSet.of(Peer.PeerRole.ENDORSING_PEER, Peer.PeerRole.LEDGER_QUERY, Peer.PeerRole.CHAINCODE_QUERY, Peer.PeerRole.EVENT_SOURCE)));
                } else {
                    // Set peer to not be all roles but eventing.
                    newChannel.joinPeer(peer, createPeerOptions().setPeerRoles(EnumSet.of(Peer.PeerRole.ENDORSING_PEER, Peer.PeerRole.LEDGER_QUERY, Peer.PeerRole.CHAINCODE_QUERY)));
                }
                everyother = !everyother;
            }


            for (Orderer orderer : orderers) { //add remaining orderers if any.
                newChannel.addOrderer(orderer);
            }

            for (Map.Entry<String, String> eventHubEntry : organization.getEventHubLocations().entrySet()) {
                String eventHubName = eventHubEntry.getKey();
                String eventHubLocation = eventHubEntry.getValue();
                final Properties eventHubProperties = configuration.getEventHubProperties(eventHubName,organization.getName());

                eventHubProperties.put("grpc.NettyChannelBuilderOption.keepAliveTime", new Object[] { 5L, TimeUnit.MINUTES});
                eventHubProperties.put("grpc.NettyChannelBuilderOption.keepAliveTimeout", new Object[] {8L, TimeUnit.SECONDS});

                EventHub eventHub = client.newEventHub(eventHubName, eventHubLocation,
                                                       eventHubProperties);
                newChannel.addEventHub(eventHub);
            }

            newChannel.initialize();


            //Just checks if channel can be serialized and deserialized .. otherwise this is just a waste :)
            byte[] serializedChannelBytes = newChannel.serializeChannel();
            newChannel.shutdown(true);

            FabricManager.getSampleStore().saveChannel(client.deSerializeChannel(serializedChannelBytes).initialize());
        }

    }
}
